/**
 * File: FileIndexWriterImpl.java
 * Created by: mhaimel
 * Created on: 15 Jul 2009
 * CVS:  $Id: FileIndexWriterImpl.java,v 1.3 2009/12/07 10:15:19 mhaimel Exp $
 */
package uk.ac.ebi.curtain.util.impl;

import static uk.ac.ebi.curtain.util.FileIndexUtil.CATEGORY_HEADER_Str;
import static uk.ac.ebi.curtain.util.FileIndexUtil.CONTIG_HEADER_Str;
import static uk.ac.ebi.curtain.util.FileIndexUtil.CONTIG_POS_HEADER_Str;
import static uk.ac.ebi.curtain.util.FileIndexUtil.FILE_HEADER_Str;
import static uk.ac.ebi.curtain.util.FileIndexUtil.INSLEN_HEADER_Str;
import static uk.ac.ebi.curtain.util.FileIndexUtil.INS_SD_HEADER_Str;
import static uk.ac.ebi.curtain.util.FileIndexUtil.READ_HEADER_Str;
import static uk.ac.ebi.curtain.util.FileIndexUtil.READ_POS_HEADER_Str;
import static uk.ac.ebi.curtain.util.FileIndexUtil.SEP_CHAR;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.NotImplementedException;
import org.apache.commons.lang.StringUtils;

import uk.ac.ebi.curtain.util.FileIndexReader;
import uk.ac.ebi.curtain.util.FileIndexWriter;
import uk.ac.ebi.curtain.util.FileIndexReader.IndexFilter;
import uk.ac.ebi.curtain.util.FileIndexUtil.FileIndexEvent;
import uk.ac.ebi.curtain.utils.CurtainUncheckedException;
import uk.ac.ebi.curtain.utils.concurrent.UtilsCollect;
import uk.ac.ebi.curtain.utils.file.FileInfo;
import uk.ac.ebi.curtain.utils.io.impl.FileIO;
import uk.ac.ebi.velvet.model.Strand;

/**
 * @author mhaimel
 *
 */
public class FileIndexWriterImpl implements FileIndexWriter {
	private final File idxFile;
	private final Object _LOCK_OBJ = new Object();
	private final AtomicReference<PrintWriter> out = new AtomicReference<PrintWriter>();
	private final Map<FileInfo, Integer> infoIdx = UtilsCollect.newConcurrentMap();
	private final Set<FileInfo> fileCat = new CopyOnWriteArraySet<FileInfo>();
	private final Set<Integer> insCat = new CopyOnWriteArraySet<Integer>();
	private final Set<Integer> sdCat = new CopyOnWriteArraySet<Integer>();
	private final Set<Integer> catId = new CopyOnWriteArraySet<Integer>();

	public FileIndexWriterImpl(File idxFile) {
		this.idxFile = idxFile;
	}
	
	private void _openWrite(boolean append) throws IOException {
		if(null != out.get()){
			throw new CurtainUncheckedException("Writer already open!!!");
		}
		out.set(new FileIO(idxFile, append).getPrintWriter());
	}

	public void close() throws IOException {
		IOUtils.closeQuietly(out.getAndSet(null));
	}

	public void addInsertLength(Integer category, Integer insLen) {
		synchronized (_LOCK_OBJ) {
			if(!insCat.contains(category)){
				_writeGeneric(INSLEN_HEADER_Str, category.toString(),insLen.toString());
				insCat.add(category);
			}
		}
	}

	public void addInsertLengthSd(Integer category, Integer sd) {
		synchronized (_LOCK_OBJ) {
			if(!sdCat.contains(category)){
				_writeGeneric(INS_SD_HEADER_Str, category.toString(), sd.toString());
				sdCat.add(category);
			}
		}
	}

	private Integer iInfo = 0;

	public void addFileId(Integer fid, FileInfo fi) {
		infoIdx.put(fi, fid);	
	}


	/**
	 * Same as {@link #registerFile(FileInfo, Integer)}, just without id value
	 * {@inheritDoc}
	 */
	public void addFile(FileInfo fi, Integer category) {
		registerFile(fi, category);
	}
	
	/**
	 * Same as {@link #addFile(FileInfo, Integer)}, just with the internal file reference id returned,
	 * which can be used for {@link #addRead(Integer, String, Strand, Long) to increase performance
	 * @param fi {@link FileInfo}
	 * @param category Category
	 * @return Integer id for file, used internally to reference the file
	 */
	public Integer registerFile(FileInfo fi, Integer category) {
		synchronized (_LOCK_OBJ) {
			Integer id = infoIdx.get(fi);
			boolean isWritten = fileCat.contains(fi);
			if (null == id) {
				if(isWritten){
					throw new CurtainUncheckedException("Entry written, but not registered - internal problem!!! " + fi + " - " + category);
				}
				id = iInfo++;
				infoIdx.put(fi, id);
			}
			if(!isWritten){
				_writeFile(id, fi, category);
				fileCat.add(fi);
			}
			return id;
		}
	}

	public void addContig(FileInfo fi, String key, Long pos) {
		synchronized (_LOCK_OBJ) {
			Integer id = _getId(fi);
			_write(CONTIG_HEADER_Str,id, key, Strand.UNKOWN, pos);
		}
	}
	

	public void addContig(Integer fileId, String key, Long pos) {
		synchronized (_LOCK_OBJ) {
			_write(CONTIG_HEADER_Str,fileId, key, Strand.UNKOWN, pos);
		}
	}

	public void addRead(FileInfo fi, String key, Strand strand, Long pos) {
		synchronized (_LOCK_OBJ) {
			Integer id = _getId(fi);
			_write(READ_HEADER_Str, id, key, strand, pos);
		}
	}

	public void addRead(Integer fId, String key, Strand strand, Long pos) {
		synchronized (_LOCK_OBJ) {
			_write(READ_HEADER_Str, fId, key, strand, pos);
		}
	}

	public void addReadBatch(Integer[] fIds, String[] key, Strand[] strand,
			Long[] pos) {
		boolean valid = false;
		if (key.length == strand.length 
				&& key.length == pos.length 
				&& key.length == fIds.length) {
			valid = true;
		}
		if (!valid) {
			throw new CurtainUncheckedException("Different lengths of arrays: "
					+ fIds.length + " : "+ key.length + " : " + strand.length + " : " + pos.length);
		}
		synchronized (_LOCK_OBJ) {
			_write(READ_HEADER_Str, fIds, key, strand, pos);
		}
	}

	public void addContigPosition(Integer fileId, Long pos) {
		synchronized (_LOCK_OBJ) {
			_write(CONTIG_POS_HEADER_Str, fileId,Strand.UNKOWN, pos);
		}
	}


	public void addContigPosition(FileInfo fi, Long pos) {
		synchronized (_LOCK_OBJ) {
			Integer fId = _getId(fi);
			_write(CONTIG_POS_HEADER_Str, fId,Strand.UNKOWN, pos);
		}
	}

	public void addReadPosition(Integer fileId, Strand strand, Long pos) {
		synchronized (_LOCK_OBJ) {
			_write(READ_POS_HEADER_Str, fileId,strand,pos);
		}
	}


	public void addReadPosition(FileInfo fi, Strand strand, Long pos) {
		synchronized (_LOCK_OBJ) {
			Integer fId = _getId(fi);
			_write(READ_POS_HEADER_Str, fId,strand,pos);
		}
	}


	@Override
	public void addCategory(Integer category, String id) {
		synchronized (_LOCK_OBJ) {
			if(!catId.contains(category)){
				_write(CATEGORY_HEADER_Str, category.toString(),id);
				catId.add(category);
			}
		}
	}

	private PrintWriter _getWriter(){
		if(null == out.get()){
			try {
				_openWrite(true);
			} catch (IOException e) {
				throw new CurtainUncheckedException(e);
			}
		}
		return out.get();
	}
	
	private void _writeGeneric(String... strArr) {
		_getWriter().print(StringUtils.join(strArr, SEP_CHAR));
		_getWriter().println();
	}

	private void _writeFile(Integer id, FileInfo fi, Integer category) {
		_writeGeneric(
				FILE_HEADER_Str, 
				id.toString(), 
				fi.getFile().getAbsolutePath(), 
				category.toString(),
				fi.getReadType().toString(), 
				fi.getFileType().toString()
				);
	}

	private void _write(String header, Integer[] ids, String[] key,
			Strand[] strand, Long[] pos) {
		for (int i = 0; i < key.length; ++i) {
			_write(header, ids[i], key[i], strand[i], pos[i]);
		}
	}
	
	private void _write(String header, Integer id, Strand strand, Long pos) {
		PrintWriter writer = _getWriter();
		writer.print(header);
		writer.print(SEP_CHAR);
		writer.print(id.toString());
		writer.print(SEP_CHAR);
		writer.print(pos.toString());
		writer.print(SEP_CHAR);
		writer.print(Character.toString(strand.getSymbol()));
		writer.println();
	}

	private void _write(String header, Integer id, String key, Strand strand,Long pos) {
		PrintWriter writer = _getWriter();
		writer.print(header);
		writer.print(SEP_CHAR);
		writer.print(key);
		writer.print(SEP_CHAR);
		writer.print(Character.toString(strand.getSymbol()));
		writer.print(SEP_CHAR);
		writer.print(id.toString());
		writer.print(SEP_CHAR);
		writer.print(pos.toString());
		writer.println();
	}
	
	private void _write(String ... arrArr){
		PrintWriter writer = _getWriter();
		int i = 0;
		int len = arrArr.length;
		if(len > 0){
			writer.print(arrArr[i++]);
		}
		while(i < len){
			writer.print(SEP_CHAR);
			writer.print(arrArr[i++]);
		}
		writer.println();
	}

	private Integer _getId(FileInfo fi) {
		Integer id = infoIdx.get(fi);
		if (null == id) {
			throw new CurtainUncheckedException("File not in Index: " + fi);
		}
		return id;
	}
	
	

	public void synchronize(FileIndexReader reader) {
		MyHandler handler = new MyHandler();
		IndexFilter filter = new IndexFilter();
		filter.allowAll(false);
		filter.allowFileInfo(true);
		reader.setEventHandler(handler, filter);
		try {
			reader.readAll();
		} catch (IOException e) {
			throw new CurtainUncheckedException("Problems synchronizing reader with writer!!!",e);
		};
	}
	
	private class MyHandler implements FileIndexEvent{

		public void addContig(FileInfo fi, String key, Long pos) {
			throw new NotImplementedException("Event not registered");
		}

		public void addContigPosition(FileInfo fi, Long pos) {
			throw new NotImplementedException("Event not registered");
		}

		public void addFile(FileInfo fi, Integer category) {
			Integer id = infoIdx.get(fi);
			if (null == id) {
				id = iInfo++;
				infoIdx.put(fi, id);
			}
			fileCat.add(fi);
		}

		public void addInsertLength(Integer category, Integer insLen) {
			insCat.add(category);
		}

		public void addInsertLengthSd(Integer category, Integer sd) {
			sdCat.add(category);
		}

		public void addRead(FileInfo fi, String key, Strand strand, Long pos) {
			throw new NotImplementedException("Event not registered");
		}

		public void addReadPosition(FileInfo fi, Strand strand, Long pos) {
			throw new NotImplementedException("Event not registered");
		}

		public void close() throws IOException {
			// do nothing
		}

		public void addFileId(Integer fid, FileInfo fi) {
			infoIdx.put(fi, fid);
		}

		@Override
		public void addCategory(Integer category, String id) {
			catId.add(category);
		}
		
	}
}
